﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Deployment.WindowsInstaller;
using System.Reflection;
using System.Diagnostics;
using System.IO;
using System.Xml.Serialization;
using System.Globalization;
using System.Security.Permissions;

namespace DeploymentCustomActions
{

    [Serializable]
    public class BiztalkApplication
    {
        public BiztalkApplication() { Resources = new List<BiztalkResource>(); References = new List<string>(); }
        public string ComponentName { get; set; }
        public string ApplicationName { get; set; }
        public string BiztalkManagementInstanceName { get; set; }
        public string BiztalkManagementDatabaseName { get; set; }
        public string BackupLocation { get; set; }
        public List<string> References { get; set; }
        public List<BiztalkResource> Resources { get; set; }   
    }

    [Serializable]
    public class BiztalkResource
    {
        public string BiztalkResourceType { get; set; }
        public string FilePath { get; set; }
        public bool GacOnInstall { get; set; }
        public bool GacOnAdd { get; set; }
    }

    public sealed class InstallBiztalk
    {
        private InstallBiztalk() {}
        [CustomAction]
        public static ActionResult CreateBiztalkApplicationExecute(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);
            Dictionary<string, BiztalkApplication> biztalkApplications = new Dictionary<string, BiztalkApplication>();
            ParseBiztalkApplicationsTable(session, ref biztalkApplications);
            ParseBiztalkApplicationResourcesTables(session, ref biztalkApplications);
            ParseBiztalkApplicationReferencesTables(session, ref biztalkApplications);

            
            foreach (BiztalkApplication applicationToRepair in biztalkApplications.Values
                .Where(s => session.Components[s.ComponentName].RequestState == InstallState.Local 
                         && session.Components[s.ComponentName].CurrentState == InstallState.Local))
            {
                applicationToRepair.BackupLocation = applicationToRepair.BackupLocation.Replace(".msi", Constants.RepairSuffix);
            }
            
            List<BiztalkApplication> applicationsToInstall = biztalkApplications.Values
                .Where(s => session.Components[s.ComponentName].RequestState == InstallState.Local)
                .ToList();

            CustomActionData customActionData = new CustomActionData();
            customActionData.AddObject(Constants.BiztalkApplication.BiztalkApplications, applicationsToInstall);
            customActionData.Add(Constants.BiztalkInstallPath, session[Constants.BiztalkInternalInstallDir]);

            session.DoAction("RollbackCreateBiztalkApplication", customActionData);
            session.DoAction("DeferredCreateBiztalkApplication", customActionData);
            session.DoAction("CommitCreateBiztalkApplication", customActionData);

            List<BiztalkApplication> applicationsToUninstall = biztalkApplications.Values
                .Where(s => session.Components[s.ComponentName].RequestState == InstallState.Absent)
                .ToList();

            CustomActionData uninstallCustomActionData = new CustomActionData();
            uninstallCustomActionData.AddObject(Constants.BiztalkApplication.BiztalkApplications, applicationsToUninstall);
            uninstallCustomActionData.Add(Constants.BiztalkInstallPath, session[Constants.BiztalkInternalInstallDir]);
            session.DoAction("CommitDeleteBiztalkApplication", uninstallCustomActionData);

            applicationsToUninstall.ForEach(s => s.BackupLocation = System.IO.Path.GetTempPath() + "\\" + s.ApplicationName + Constants.UninstallSuffix);

            uninstallCustomActionData = new CustomActionData();
            uninstallCustomActionData.AddObject(Constants.BiztalkApplication.BiztalkApplications, applicationsToUninstall);
            uninstallCustomActionData.Add(Constants.BiztalkInstallPath, session[Constants.BiztalkInternalInstallDir]);

            session.DoAction("RollbackDeleteBiztalkApplication", uninstallCustomActionData);
            session.DoAction("DeferredDeleteBiztalkApplication", uninstallCustomActionData);
            return ActionResult.Success;
        }

        [CustomAction]
        public static ActionResult CommitCreateBiztalkApplication(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);
            List<BiztalkApplication> applications = session.CustomActionData.GetObject<List<BiztalkApplication>>(Constants.BiztalkApplication.BiztalkApplications);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];

            applications.ForEach(s => CreateBiztalkApplication(s, biztalkInstallPath));
            try
            {
                applications.Where(s => s.BackupLocation.EndsWith(Constants.RepairSuffix, StringComparison.OrdinalIgnoreCase))
                    .Select(s => s.BackupLocation).ToList().ForEach(File.Delete);
            }
            catch (Exception ex)
            {
                session.Log(ex.Message ?? string.Empty);                
                session.Log(ex.StackTrace);
                session.Log("Ignoring error : Its only removing backup files");
            }
            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult DeferredCreateBiztalkApplication(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);
            List<BiztalkApplication> applications = session.CustomActionData.GetObject<List<BiztalkApplication>>(Constants.BiztalkApplication.BiztalkApplications);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];

            foreach (BiztalkApplication application in applications)
            {
                if (BiztalkExistsInSystem(application, biztalkInstallPath))
                {
                    if (BackupExistingBiztalkApplications(application, biztalkInstallPath, session) != 0)
                        return ActionResult.Failure;
                    if (DeleteBiztalkApplication(application, biztalkInstallPath, session) != 0)
                        return ActionResult.Failure;
                }
            }
            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult RollbackCreateBiztalkApplication(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);

            List<BiztalkApplication> biztalkApplications = session.CustomActionData.GetObject<List<BiztalkApplication>>(Constants.BiztalkApplication.BiztalkApplications);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];
            biztalkApplications.ForEach(s => RestoreBiztalkApplication(s, biztalkInstallPath, session));
            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult CommitDeleteBiztalkApplication(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);
            List<BiztalkApplication> applications = session.CustomActionData.GetObject<List<BiztalkApplication>>(Constants.BiztalkApplication.BiztalkApplications);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];

            foreach (BiztalkApplication application in applications.Where(s => File.Exists(s.BackupLocation)))
            {
                try
                {
                    RestoreBiztalkApplication(application, biztalkInstallPath, session);
                }
                catch (Exception ex)
                {
                    session.Log(ex.Message ?? String.Empty);
                    session.Log(ex.StackTrace ?? String.Empty);
                }
            }

            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult DeferredDeleteBiztalkApplication(Session session)
        {
            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);
            return DeferredCreateBiztalkApplication(session);
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult RollbackDeleteBiztalkApplication(Session session)
        {
            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);
            return RollbackCreateBiztalkApplication(session);
        }

        [CustomAction]
        public static ActionResult ErrorOut(Session session)
        {
            if (session == null) return ActionResult.Failure;
            return ActionResult.Failure;
        }

        private static bool BiztalkExistsInSystem(BiztalkApplication biztalkApplication, string biztalkInstallPath)
        {
            dynamic explorer = InitializeBiztalkCatalogExplorer(biztalkInstallPath);
            explorer.ConnectionString = String.Format(CultureInfo.InvariantCulture, Constants.BiztalkCatalogUrl, biztalkApplication.BiztalkManagementInstanceName, biztalkApplication.BiztalkManagementDatabaseName);
            return explorer.Applications[biztalkApplication.ApplicationName] != null;
        }

        private static int BackupExistingBiztalkApplications(BiztalkApplication biztalkApplication, string biztalkInstallPath, Session session)
        {
            string arguments = String.Format(CultureInfo.InvariantCulture, "ExportApp -ApplicationName:\"{0}\" -Package:\"{1}\" -Server:{2} -Database:{3}",
                    biztalkApplication.ApplicationName, 
                    biztalkApplication.BackupLocation, 
                    biztalkApplication.BiztalkManagementInstanceName, 
                    biztalkApplication.BiztalkManagementDatabaseName);

            return ExecuteBTSTask(arguments, biztalkInstallPath, session);
        }

        private static int RestoreBiztalkApplication(BiztalkApplication biztalkApplication, string biztalkInstallPath, Session session)
        {
            if (BiztalkExistsInSystem(biztalkApplication, biztalkInstallPath)) DeleteBiztalkApplication(biztalkApplication, biztalkInstallPath, session);

            if (File.Exists(biztalkApplication.BackupLocation))
            {
                string arguments = String.Format(CultureInfo.InvariantCulture, "ImportApp -ApplicationName:\"{0}\" -Package:\"{1}\" -Server:{2} -Database:{3} -Overwrite",
                        biztalkApplication.ApplicationName,
                        biztalkApplication.BackupLocation,
                        biztalkApplication.BiztalkManagementInstanceName,
                        biztalkApplication.BiztalkManagementDatabaseName);

                int exitCode = ExecuteBTSTask(arguments, biztalkInstallPath, session);
                File.Delete(biztalkApplication.BackupLocation);
                return exitCode;
            }
            return 0;
        }

        private static int ExecuteBTSTask(string arguments, string biztalkInstallPath, Session session)
        {
            session.Log("\n\nExecuting BTSTask.exe {0}\n\n", arguments);
            Process process = null;
            try
            {
                process = new Process();
                process.StartInfo.WorkingDirectory = biztalkInstallPath;
                process.StartInfo.Arguments = arguments;
                process.StartInfo.FileName = "BTSTask.exe";
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;

                process.OutputDataReceived += (x, y) => session.Log(y.Data ?? String.Empty);
                process.ErrorDataReceived += (x, y) => session.Log(y.Data ?? String.Empty);
                process.EnableRaisingEvents = true;
                process.Start();
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();
                process.WaitForExit();
                int exitCode = process.ExitCode;

                return exitCode;
            }
            finally
            {
                process.Close();
            }
        }

        private static int DeleteBiztalkApplication(BiztalkApplication biztalkApplication, string biztalkInstallationPath, Session session)
        {
            dynamic btsCExp = InitializeBiztalkCatalogExplorer(biztalkInstallationPath);
            try {   
                btsCExp.ConnectionString = String.Format(CultureInfo.InvariantCulture, Constants.BiztalkCatalogUrl, biztalkApplication.BiztalkManagementInstanceName, biztalkApplication.BiztalkManagementDatabaseName);
                var application = btsCExp.Applications[biztalkApplication.ApplicationName];

                dynamic stopOption = Enum.Parse(application.GetType().Assembly.GetType("Microsoft.BizTalk.ExplorerOM.ApplicationStopOption"), "StopAll");
                application.Stop(stopOption);
                btsCExp.SaveChanges(); 
            } catch (Exception)
            {
                btsCExp.DiscardChanges();
                throw;
            } finally { btsCExp.Dispose(); }

            string arguments = String.Format(CultureInfo.InvariantCulture, "RemoveApp -ApplicationName:\"{0}\" -Server:{1} -Database:{2}",
                biztalkApplication.ApplicationName,
                biztalkApplication.BiztalkManagementInstanceName,
                biztalkApplication.BiztalkManagementDatabaseName);
            return ExecuteBTSTask(arguments, biztalkInstallationPath, session);

        }

        private static void ParseBiztalkApplicationsTable(Session session, ref Dictionary<string, BiztalkApplication> biztalkApplications)
        {
            using (View view = session.Database.OpenView(Constants.BiztalkApplication.Query))
            {
                view.Execute();
                foreach (Record record in view) using (record)
                    {
                        string id = record.GetString(Constants.BiztalkApplication.Id);
                        string applicationName = session.Format(record.GetString(Constants.BiztalkApplication.ApplicationName));
                        string biztalkManagementInstanceName = session.Format(record.GetString(Constants.BiztalkApplication.BiztalkManagementInstanceName));
                        string BiztalkManagementDatabaseName = session.Format(record.GetString(Constants.BiztalkApplication.BiztalkManagementDatabaseName));
                        string componentName = session.Format(record.GetString(Constants.BiztalkApplication.ComponentName));
                        string backupLocation = session.Format(record.GetString(Constants.BiztalkApplication.BackupLocation));

                        BiztalkApplication biztalkApplication = new BiztalkApplication()
                        {
                            ApplicationName = applicationName,
                            BiztalkManagementInstanceName = biztalkManagementInstanceName,
                            BiztalkManagementDatabaseName = BiztalkManagementDatabaseName,
                            ComponentName = componentName,
                            BackupLocation = String.Format(CultureInfo.InvariantCulture, "{0}{1}.msi", backupLocation, applicationName)
                        };
                        biztalkApplications.Add(id, biztalkApplication);
                    }
            }
        }

        private static void ParseBiztalkApplicationResourcesTables(Session session, ref Dictionary<string, BiztalkApplication> biztalkApplications)
        {
            using (View view = session.Database.OpenView(Constants.BiztalkApplicationResources.Query))
            {
                view.Execute();
                foreach (Record record in view) using (record)
                    {
                        string biztalkApplicationId = record.GetString(Constants.BiztalkApplicationResources.BiztalkApplicationId);
                        string filePath = record.GetString(Constants.BiztalkApplicationResources.FilePath);
                        filePath = session.Format(filePath);
                        string biztalkApplicationType = record.GetString(Constants.BiztalkApplicationResources.BiztalkResourceType);

                        BiztalkResource resource = new BiztalkResource()
                        {
                            BiztalkResourceType = biztalkApplicationType,
                            FilePath = filePath,
                            GacOnAdd = true,
                            GacOnInstall = true
                        };
                        biztalkApplications[biztalkApplicationId].Resources.Add(resource);
                    }
            }
        }

        private static void ParseBiztalkApplicationReferencesTables(Session session, ref Dictionary<string, BiztalkApplication> biztalkApplications)
        {
            using (View view = session.Database.OpenView(Constants.BiztalkApplicationReferences.Query))
            {
                view.Execute();
                foreach (Record record in view) using (record)
                    {
                        string biztalkApplicationId = record.GetString(Constants.BiztalkApplicationReferences.BiztalkApplicationId);
                        string applicationName = session.Format(record.GetString(Constants.BiztalkApplicationReferences.ApplicationName));
                        biztalkApplications[biztalkApplicationId].References.Add(applicationName);
                    }
            }
        }

        private static void CreateBiztalkApplication(BiztalkApplication application, string biztalkInstallationPath)
        {
            dynamic btsCExp = 
                // (Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer)
                InitializeBiztalkCatalogExplorer(biztalkInstallationPath);
            try
            {
                btsCExp.ConnectionString = String.Format(CultureInfo.InvariantCulture, Constants.BiztalkCatalogUrl, application.BiztalkManagementInstanceName, application.BiztalkManagementDatabaseName);
                dynamic biztalkApplication = btsCExp.AddNewApplication();
                biztalkApplication.Name = application.ApplicationName;
                biztalkApplication.Description = application.ApplicationName;
                foreach (string reference in application.References)
                {
                    biztalkApplication.AddReference(btsCExp.Applications[reference]);
                }
                btsCExp.SaveChanges();
            }
            catch (Exception)
            {
                btsCExp.DiscardChanges();                
                throw;
            }
            finally
            {
                btsCExp.Dispose();
            }
            AddBiztalkResourcesToApplication(application, biztalkInstallationPath);
        }

        private static void AddBiztalkResourcesToApplication(BiztalkApplication application, string biztalkInstallationPath)
        {                      
            dynamic lDepGroup = InitializeApplicationDeploymentGroup(biztalkInstallationPath);

            try
            {
                lDepGroup.DBServer = application.BiztalkManagementInstanceName;
                lDepGroup.DBName = application.BiztalkManagementDatabaseName;
                
                dynamic biztalkApplication = lDepGroup.Applications[application.ApplicationName];

                if (biztalkApplication.IsSystem)
                    throw new ArgumentException(Constants.BiztalkApplicationResources.ErrorSystemApplicationUpdate);

                foreach (BiztalkResource resource in application.Resources)
                {
                    string destination = string.Format(CultureInfo.InvariantCulture, Constants.BiztalkApplicationResources.BiztalkResourceDestinationFormat, System.IO.Path.GetFileName(resource.FilePath));
                    Dictionary<string, object> requestProperties = new Dictionary<string, object>();

                    requestProperties.Add("GacOnAdd", resource.GacOnAdd);
                    requestProperties.Add("GacOnInstall", resource.GacOnInstall);

                    biztalkApplication.ResourceCollection.Add(resource.BiztalkResourceType, resource.FilePath, destination, requestProperties, false);
                }
            }
            catch
            {
                lDepGroup.Abort();
                throw;
            }
            finally
            {
                lDepGroup.Dispose();
            }

        }

        private static object InitializeBiztalkCatalogExplorer(string biztalkInstallationPath)
        {
            Assembly biztalkExplorerOMAssembly = Assembly.Load(new AssemblyName { CodeBase = biztalkInstallationPath + @"\Developer Tools\Microsoft.BizTalk.ExplorerOM.dll" });
            return biztalkExplorerOMAssembly.CreateInstance("Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer");
        }

        private static object InitializeApplicationDeploymentGroup(string biztalkInstallationPath)
        {
            Assembly biztalkExplorerOMAssembly = Assembly.Load(new AssemblyName { CodeBase = biztalkInstallationPath + @"\Microsoft.BizTalk.ApplicationDeployment.Engine.dll" });
            return biztalkExplorerOMAssembly.CreateInstance("Microsoft.BizTalk.ApplicationDeployment.Group");
        }

        private static object InitializeApplicationDeploymentApplication(string biztalkInstallationPath)
        {
            Assembly biztalkExplorerOMAssembly = Assembly.Load(new AssemblyName { CodeBase = biztalkInstallationPath + @"\Microsoft.BizTalk.ApplicationDeployment.Engine.dll" });
            return biztalkExplorerOMAssembly.CreateInstance("Microsoft.BizTalk.ApplicationDeployment.Application");
        }

        private static void LogMethodEntry(Session session, string methodName)
        {
            session.Log("Begin {0}", methodName);
        }

    }
}
